package controller;

import java.util.ArrayList;
import java.util.Map;

import common.*;
import data.*;
import model.*;
import builder.*;

/**
 * 
 * This class serves as the Controller for the application.
 * 
 * @author Roman Pusec, Dennis Goldfinger, Marko Bilic
 *
 */
public class Controller implements IObservable {
	
	//DAOs
	private DailyLogDao dailyLogDao;
	private BasicDao basicDao;
	private RecipeDao recipeDao;
	private BasicExerciseDao basicExerciseDao;
	private WorkoutDao workoutDao;
	
	//Arrays
	private ArrayList<IObserver> observers;
	private ArrayList<Basic> basicList;
	private ArrayList<Recipe> recipeList;
	private ArrayList<Exercise> exerciseList;
	private ArrayList<Workout> workoutList;
	private ArrayList<DailyLog> dailyLogs;
	
	private int currentDailyLogIndex; //current daily log
	private FileBuilder fileBuilder; //file builder
	
	/**
	 * Parameterized controller, constructs DAOs, Arrays, etc. 
	 * 
	 * @param dailyLogPath 	Path to the DailyLog xml file
	 * @param basicPath 	Path to the Basic Food xml file
	 * @param recipePath 	Path to the Recipes xml file
	 * @param exercisePath 	Path to the Exercises xml file
	 * @param workoutPath 	Path to the Workouts xml file
	 */
	public Controller(String dailyLogPath, String basicPath, String recipePath, String exercisePath, String workoutPath)
	{
		this.dailyLogDao = new DailyLogDao(dailyLogPath);
		
		this.basicDao = new BasicDao(basicPath);
		this.recipeDao = new RecipeDao(recipePath);
		
		this.basicExerciseDao = new BasicExerciseDao(exercisePath);
		this.workoutDao = new WorkoutDao(workoutPath);
		
		dailyLogs = dailyLogDao.getAll(); 
		
		basicList = basicDao.getAll();
		recipeList = recipeDao.getAll();
		
		exerciseList = basicExerciseDao.getAll();
		workoutList = workoutDao.getAll(); 
		
		observers = new ArrayList<IObserver>();
	}
	
	/**
	 * Sets the DailyLog.
	 * 
	 * @param date Date of the DailyLog entry. 
	 */
	public void selectDate(String date)
	{
		setDailyLog(date);
		notifyAllObservers();
	}
	
	/**
	 * Sets the daily log based on the date that the user specified. 
	 * 
	 * @param date Date of the DailyLog entry. 
	 */
	private void setDailyLog(String date)
	{
		//if the dailyLogs size is zero, the program
		//creates a new DailyLog object with default
		//calorie and limit
		if(dailyLogs.size() == 0)
		{
			dailyLogs.add(new DailyLog(160, 1800, date));
			currentDailyLogIndex = dailyLogs.size()-1;
		}
		else
		{
			//loops through the daily log objects
			for(DailyLog dl : dailyLogs)
			{
				//if a daily log object with the same
				//date already exists, we should modify
				//that specific log object from 
				//the ArrayList
				if(dl.getDate().equals(date))
				{
					this.currentDailyLogIndex = dailyLogs.indexOf(dl);
					return; //ends the method execution
				}
			}
			
			//if there's no log with the given date, the program should
			//create a new log with the weight and limit of the last log
			dailyLogs.add(new DailyLog(dailyLogs.get(dailyLogs.size()-1).getWeight(), dailyLogs.get(dailyLogs.size()-1).getLimit(), date));
			currentDailyLogIndex = dailyLogs.size()-1;
		}
	}
	
	/**
	 * Cancels the current log and all of the changes are trashed out. 
	 */
	public void cancelCurrentLog()
	{
		// cancels editing of current log and trashes it, only effective when not saved
		dailyLogs = dailyLogDao.getAll();
		currentDailyLogIndex = 0;
		notifyAllObservers();
	}
	
	/**
	 * Gets the currently selected daily log. 
	 * @return Currently selected Daily Log object. 
	 */
	public DailyLog getCurrDailyLog()
	{
		// returns current daily log
		return dailyLogs.get(currentDailyLogIndex);
	}
	
	/**
	 * Returns the Basic Foods Array List. 
	 * @return Basic Foods Array List. 
	 */
	public ArrayList<Basic> getBasicList()
	{
		// returns list of basic foods
		return basicList;
	}
	
	/**
	 * Returns the Recipe Array List. 
	 * @return Recipe Array List. 
	 */
	public ArrayList<Recipe> getRecipeList()
	{
		// returns list of recipes
		return recipeList;
	}
	
	/**
	 * Returns the Exercises Array List. 
	 * @return Exercises Array List. 
	 */
	public ArrayList<Exercise> getBasicExerciseList()
	{
		// returns list of basic exercises
		return exerciseList;
	}
	
	/**
	 * Returns the Workout Array List. 
	 * @return Workout Array List. 
	 */
	public ArrayList<Workout> getWorkoutList()
	{
		// returns list of workouts
		return workoutList;
	}
	
	/**
	 * Adds the food to the currently selected DailyLog. 
	 * @param food Food to add. 
	 * @param quantity The quantity of food. 
	 */
	public void addFoodToLog(Food food, int quantity)
	{
		//updates the intake from dailyLog
		dailyLogs.get(currentDailyLogIndex).addToIntake(food, quantity);
		notifyAllObservers();
	}
	
	/**
	 * Deletes the food from the currently selected DailyLog. 
	 * @param food Food to delete. 
	 */
	public void deleteFoodFromLog(Food food)
	{
		//updates the intake from dailyLog
		dailyLogs.get(currentDailyLogIndex).removeFromIntake(food);
		notifyAllObservers();
	}
	
	/**
	 * Adds exercise to the currently selected DailyLog. 
	 * @param exercise Exercise to add.
	 * @param quantity The quantity of exercise. 
	 */
	public void addExerciseToLog(Activity exercise, int quantity)
	{
		//updates the intake from dailyLog
		dailyLogs.get(currentDailyLogIndex).addToActivities(exercise, quantity);
		notifyAllObservers();
	}
	
	/**
	 * Deletes the exercise from the currently selected DailyLog. 
	 * @param exercise The exercise to delete from log. 
	 */
	public void deleteExerciseFromLog(Activity exercise)
	{
		//updates the intake from dailyLog
		dailyLogs.get(currentDailyLogIndex).removeFromActivities(exercise);
		notifyAllObservers();
	}
	
	/**
	 * Changes the limit of the currently selected DailyLog. 
	 * @param newLimit The new limit. 
	 */
	public void changeCalorieLimit(double newLimit)
	{
		// updates calorie limit
		dailyLogs.get(currentDailyLogIndex).setLimit(newLimit);
		notifyAllObservers();
	}
	
	/**
	 * Changes the weight of the currently selected DailyLog. 
	 * @param newWeight The new weight. 
	 */
	public void changeWeight(double newWeight)
	{
		// updates weight
		dailyLogs.get(currentDailyLogIndex).setWeight(newWeight);
		notifyAllObservers();
	}
	
	/**
	 * Adds the Food (it can be either basic or recipe)
	 * @param food The food to add. 
	 */
	public void addFood(Food food)
	{
		//adds the food in the appropriate ArrayList
		if(food instanceof Basic)
			basicList.add((Basic)food);
		else if(food instanceof Recipe)
			recipeList.add((Recipe)food);
	}
	
	/**
	 * Deletes the Food (it can be either basic or recipe)
	 * @param food The food to delete. 
	 */
	public void deleteFood(Food food)
	{
		//removes the food from the appropriate ArrayList
		if(food instanceof Basic)
			if(basicList.contains(food))
				basicList.remove((Basic)food);
			
		else if(food instanceof Recipe)
			if(recipeList.contains(food))
				recipeList.remove((Recipe)food);
	}
	
	/**
	 * Adds the Activity (either Exercises or Workouts)
	 * @param activity Activity to add
	 */
	public void addExercise(Activity activity)
	{
		//adds the exercise in the appropriate ArrayList
		if(activity instanceof Exercise)
			exerciseList.add((Exercise)activity);
		else if(activity instanceof Workout)
			workoutList.add((Workout)activity);
	}
	
	/**
	 * Deletes the Activity (either Exercises or Workouts)
	 * @param activity Activity to delete
	 */
	public void deleteExercise(Activity exercise)
	{
		//removes the exercise from the appropriate ArrayList
		if(exercise instanceof Exercise)
			if(exerciseList.contains(exercise))
				exerciseList.remove((Exercise)exercise);
			
		else if(exercise instanceof Workout)
			if(workoutList.contains(exercise))
				workoutList.remove((Workout)exercise);
	}
	
	/**
	 * Updates all of the databases. 
	 * @throws Exception
	 */
	public void saveChanges() throws Exception
	{
		//updates all databases
		dailyLogDao.writeAll(dailyLogs);
		basicDao.writeAll(basicList);
		recipeDao.writeAll(recipeList);
		basicExerciseDao.writeAll(exerciseList);
		workoutDao.writeAll(workoutList);
	}
	
	/**
	 * Returns the total protein calories
	 * @return total protein calories
	 */
	public double getTotalProteinCalories()
	{
		// get intake from current daily log
		Map<Food, Integer> intake = dailyLogs.get(currentDailyLogIndex).getIntake();
		double protein = 0;
		
		// iterate through intake and determine whether entry is food or recipe
		for(Map.Entry<Food, Integer> entry : intake.entrySet())
		{	
			if(basicList.indexOf(entry.getKey()) != -1)
			{
				// determine protein calories from Basic food info
				Basic item = basicList.get(basicList.indexOf(entry.getKey()));
				protein += ((item.getProtein() * .01) * item.getCalories()) * entry.getValue();
			}
			else if(recipeList.indexOf(entry.getKey()) != -1)
			{
				Map<Food, Integer> foodMap = recipeList.get(recipeList.indexOf(entry.getKey())).getFoods();
				
				for(Map.Entry<Food, Integer> food : foodMap.entrySet())
				{
					if(basicList.indexOf(food.getKey()) != -1)
					{
						// determine protein calories from each individual recipe ingredient
						Basic item = basicList.get(basicList.indexOf(food.getKey()));
						protein += ((item.getProtein() * .01) * item.getCalories()) * food.getValue();
					}
				}
			}
		}
		
		return protein;
	}
	
	/**
	 * Returns the total fat calories
	 * @return total fat calories
	 */
	public double getTotalFatCalories()
	{
		// get intake from current daily log
		Map<Food, Integer> intake = dailyLogs.get(currentDailyLogIndex).getIntake();
		double fat = 0;
		
		// iterate through intake and determine whether entry is food or recipe
		for(Map.Entry<Food, Integer> entry : intake.entrySet())
		{
			if(basicList.indexOf(entry.getKey()) != -1)
			{	
				// determine fat calories from Basic food info
				Basic item = basicList.get(basicList.indexOf(entry.getKey()));
				fat += ((item.getFat() * .01) * item.getCalories()) * entry.getValue();
			}
			else if(recipeList.indexOf(entry.getKey()) != -1)
			{
				Map<Food, Integer> foodMap = recipeList.get(recipeList.indexOf(entry.getKey())).getFoods();
				
				for(Map.Entry<Food, Integer> food : foodMap.entrySet())
				{
					if(basicList.indexOf(food.getKey()) != -1)
					{	
						// determine fat calories from each individual recipe ingredient
						Basic item = basicList.get(basicList.indexOf(food.getKey()));
						fat += ((item.getFat() * .01) * item.getCalories()) * food.getValue();
					}
				}
			}
		}
		
		return fat;
	}
	
	/**
	 * Returns the total carb calories
	 * @return total carb calories
	 */
	public double getTotalCarbCalories()
	{
		// get intake from current daily log
		Map<Food, Integer> intake = dailyLogs.get(currentDailyLogIndex).getIntake();
		double carbs = 0;
		
		// iterate through intake and determine whether entry is food or recipe
		for(Map.Entry<Food, Integer> entry : intake.entrySet())
		{
			if(basicList.indexOf(entry.getKey()) != -1)
			{	
				// determine carb calories from Basic food info
				Basic item = basicList.get(basicList.indexOf(entry.getKey()));
				carbs += ((item.getCarbs() * .01) * item.getCalories()) * entry.getValue();
			}
			else if(recipeList.indexOf(entry.getKey()) != -1)
			{
				Map<Food, Integer> foodMap = recipeList.get(recipeList.indexOf(entry.getKey())).getFoods();
				
				for(Map.Entry<Food, Integer> food : foodMap.entrySet())
				{
					if(basicList.indexOf(food.getKey()) != -1)
					{
						// determine carb calories from each individual recipe ingredient
						Basic item = basicList.get(basicList.indexOf(food.getKey()));
						carbs += ((item.getCarbs() * .01) * item.getCalories()) * food.getValue();
					}
				}
			}
		}
		
		return carbs;
	}
	
	/**
	 * Returns the total calories
	 * @return total calories
	 */
	public double getTotalCalories()
	{
		// get intake from current daily log
		Map<Food, Integer> intake = dailyLogs.get(currentDailyLogIndex).getIntake();
		double calories = 0;
		
		// iterate through intake and determine whether entry is food or recipe
		for(Map.Entry<Food, Integer> entry : intake.entrySet())
		{
			if(basicList.indexOf(entry.getKey()) != -1)
			{	
				// determine total calories from Basic food info
				Basic item = basicList.get(basicList.indexOf(entry.getKey()));
				calories += item.getCalories() * entry.getValue();
			}
			else if(recipeList.indexOf(entry.getKey()) != -1)
			{
				Map<Food, Integer> foodMap = recipeList.get(recipeList.indexOf(entry.getKey())).getFoods();
				
				for(Map.Entry<Food, Integer> food : foodMap.entrySet())
				{
					if(basicList.indexOf(food.getKey()) != -1)
					{
						// determine total calories from each individual recipe ingredient
						Basic item = basicList.get(basicList.indexOf(food.getKey()));
						calories += item.getCalories() * food.getValue();
					}
				}
			}
		}
		
		return calories;
	}
	
	/**
	 * Returns the Food by Food
	 * @param food
	 * @return food
	 */
	public Food getFoodByFood(Food food)
	{
		if(basicList.indexOf(food) != -1)
		{	
			return basicList.get(basicList.indexOf(food));
		}
		else if(recipeList.indexOf(food) != -1)
		{
			return recipeList.get(recipeList.indexOf(food));
		}
		else 
		{
			return new Food("Error food");
		}
	}
	
	/**
	 * Returns the Activity by Activity
	 * @param activity
	 * @return activity
	 */
	public Activity getActivityByActivity(Activity activity)
	{
		if(exerciseList.indexOf(activity) != -1)
		{
			return exerciseList.get(exerciseList.indexOf(activity));
		}
		else if(workoutList.indexOf(activity) != -1)
		{
			return workoutList.get(workoutList.indexOf(activity));
		}
		else
		{
			return new Activity("Error activity");
		}
	}
	
	/**
	 * Sets the builder
	 * @param fb FileBuilder
	 */
	public void setFileBuilder(FileBuilder fb)
	{
		this.fileBuilder = fb;
	}
	
	/**
	 * Builds the file
	 * @return new File
	 */
	public String buildFile()
	{
		return this.fileBuilder.buildFile(dailyLogs, this);
	}

	@Override
	public void subscribe(IObserver observer) {
		if(!observers.contains(observer))
			observers.add(observer);
	}

	@Override
	public void unsubscribe(IObserver observer) {
		if(observers.contains(observer))
			observers.remove(observer);
	}

	@Override
	public void notifyAllObservers() {
		for(IObserver observer : observers)
			observer.notifyObserver();
	}
	
}